De acordo com Yann LeCun, "Adversarial training is the coolest thing since sliced bread.". O pão cortado certamente nunca criou essa excitação dentro da comunidade de aprendizado profundo. As redes adversárias generativas - ou os GANs - afiaram dramaticamente a possibilidade de conteúdo gerado por IA e desencadearam esforços de pesquisa ativos desde que foram descitos por Ian Goodfellow et al. in 2014. Os GANs são redes neurais que aprendem a criar dados sintéticos semelhantes a alguns dados de entrada conhecidos.
Generative adversarial networks consistem de 2 modelos: modelo generativo e modelo discriminativo.
O modelo discriminativo é um classificador que determina se uma determinada imagem se parece com uma imagem real do conjunto de dados ou como uma imagem criada artificialmente. Este é basicamente um classificador binário que assumirá a forma de uma rede neural convolucional normal (CNN).
O modelo generativo possui valores de entrada aleatórios e os transforma em imagens através de uma rede neural deconvolucional.
Ao longo de muitas iterações de treinamento, os pesos e os bias nos modelos discriminativo e o generativo são treinados através de backpropagation. O modelo discriminativo aprende a identificar imagens "reais" de dígitos manuscritos além das imagens "falsas" criadas pelo modelo generativo. Ao mesmo tempo, o modelo generativo usa as perdas do modelo discriminativo para aprender a produzir imagens convincentes que o modelo discriminativo não consegue mais distinguir de imagens reais.
In [1]:
!nvidia-smi
In [2]:
import numpy as np
import datetime
import tensorflow as tf
import matplotlib.pyplot as plt
%matplotlib inline
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("MNIST_data/")
A variável MNIST que criamos acima contém as imagens e os seus rótulos, divididos em um conjunto de treinamento chamado train
e um conjunto de validação chamado validation
. (Não precisamos nos preocupar com os rótulos). Podemos recuperar lotes de imagens chamando next_batch
no mnist
. Vamos carregar uma imagem.
As imagens são formatadas inicialmente como uma única linha de 784 pixels. Podemos remodelá-los em imagens de 28 x 28 pixels e visualizá-las usando pyplot.
In [3]:
sample_image = mnist.train.next_batch(1)[0]
print(sample_image.shape)
sample_image = sample_image.reshape([28, 28])
plt.imshow(sample_image, cmap='Greys')
Out[3]:
Nosso modelo discriminativo é uma rede neural convolucional que recebe uma imagem de tamanho 28 x 28 x 1 como entrada e retorna um único número escalar que descreve se a imagem de entrada é ou não "real" ou "falsa" - ou seja, seja desenhada do conjunto de imagens MNIST ou geradas pelo modelo generativo.
A estrutura do nosso modelo modelo discriminativo é baseada em TensorFlow's sample CNN classifier model. Possui duas camadas convolucionais que encontram recursos de 5x5 pixels e duas camadas "totalmente conectadas" que multiplicam pesos por cada pixel na imagem.
Para configurar cada camada, começamos por criar variáveis de peso e bias através de tf.get_variable
. Os pesos são inicializados de uma distribuição truncated normal e os bias são inicializados em zero.
tf.nn.conv2d()
é a função de convolução padrão do TensorFlow. Recebe 4 argumentos. O primeiro é o volume de entrada (nossas imagens 28 x 28 x 1
neste caso). O próximo argumento é a matriz filtro / peso. Finalmente, você também pode mudar o stride e o padding da convolução. Esses dois valores afetam as dimensões do volume de saída.
Se você já está confortável com CNNs, você reconhecerá isso como um simples classificador binário - nada extravagante.
In [4]:
def discriminator(images, reuse_variables=None):
with tf.variable_scope(tf.get_variable_scope(), reuse=reuse_variables) as scope:
# Primeira camada de convolução e de pooling
# São 32 features diferentes de 5 x 5 pixels
d_w1 = tf.get_variable('d_w1', [5, 5, 1, 32], initializer = tf.truncated_normal_initializer(stddev = 0.02))
d_b1 = tf.get_variable('d_b1', [32], initializer = tf.constant_initializer(0))
d1 = tf.nn.conv2d(input = images, filter = d_w1, strides=[1, 1, 1, 1], padding = 'SAME')
d1 = d1 + d_b1
d1 = tf.nn.relu(d1)
d1 = tf.nn.avg_pool(d1, ksize = [1, 2, 2, 1], strides = [1, 2, 2, 1], padding = 'SAME')
# Segunda camada de convolução e de pooling
# São 64 features diferentes de 5 x 5 pixels
d_w2 = tf.get_variable('d_w2', [5, 5, 32, 64], initializer = tf.truncated_normal_initializer(stddev = 0.02))
d_b2 = tf.get_variable('d_b2', [64], initializer = tf.constant_initializer(0))
d2 = tf.nn.conv2d(input = d1, filter = d_w2, strides = [1, 1, 1, 1], padding = 'SAME')
d2 = d2 + d_b2
d2 = tf.nn.relu(d2)
d2 = tf.nn.avg_pool(d2, ksize = [1, 2, 2, 1], strides = [1, 2, 2, 1], padding = 'SAME')
# Primeira camada totalmente conectada
d_w3 = tf.get_variable('d_w3', [7 * 7 * 64, 1024], initializer = tf.truncated_normal_initializer(stddev = 0.02))
d_b3 = tf.get_variable('d_b3', [1024], initializer = tf.constant_initializer(0))
d3 = tf.reshape(d2, [-1, 7 * 7 * 64])
d3 = tf.matmul(d3, d_w3)
d3 = d3 + d_b3
d3 = tf.nn.relu(d3)
# Segunda camada totalmente conectada
d_w4 = tf.get_variable('d_w4', [1024, 1], initializer = tf.truncated_normal_initializer(stddev = 0.02))
d_b4 = tf.get_variable('d_b4', [1], initializer = tf.constant_initializer(0))
d4 = tf.matmul(d3, d_w4) + d_b4
# Resultado do modelo discriminativo
return d4
Agora que temos o nosso modelo discriminativo definido, vamos dar uma olhada no modelo generativo.
Você pode pensar no modelo generativo como um tipo de rede neural convolutiva reversa (Deconvolutional). Uma CNN típica como a nossa rede discriminativa transforma uma matriz de valores de pixel de 2 ou 3 dimensões em uma única probabilidade. Um gerador, no entanto, recebe um vetor de ruído com d
dimensões e o faz subir para uma imagem de 28 x 28. A ReLU e a normalização do lote são usadas para estabilizar as saídas de cada camada.
Na nossa rede geradora, usamos três camadas convolucionais, juntamente com a interpolação, até formar uma imagem de pixel "28 x 28".
Na camada de saída, adicionamos uma função de ativação [tf.sigmoid()
(https://www.tensorflow.org/api_docs/python/tf/sigmoid). Isso "espreme" os pixels que pareceriam cinza em preto ou branco, resultando em uma imagem mais nítida.
BatchNorm: https://www.tensorflow.org/api_docs/python/tf/contrib/layers/batch_norm
In [5]:
def generator(z, batch_size, z_dim):
# Camada 1
g_w1 = tf.get_variable('g_w1', [z_dim, 3136], dtype=tf.float32, initializer=tf.truncated_normal_initializer(stddev=0.02))
g_b1 = tf.get_variable('g_b1', [3136], initializer=tf.truncated_normal_initializer(stddev=0.02))
g1 = tf.matmul(z, g_w1) + g_b1
g1 = tf.reshape(g1, [-1, 56, 56, 1])
g1 = tf.contrib.layers.batch_norm(g1, epsilon=1e-5, scope='bn1')
g1 = tf.nn.relu(g1)
# Gerando 50 features
g_w2 = tf.get_variable('g_w2', [3, 3, 1, z_dim/2], dtype=tf.float32, initializer=tf.truncated_normal_initializer(stddev=0.02))
g_b2 = tf.get_variable('g_b2', [z_dim/2], initializer=tf.truncated_normal_initializer(stddev=0.02))
g2 = tf.nn.conv2d(g1, g_w2, strides=[1, 2, 2, 1], padding='SAME')
g2 = g2 + g_b2
g2 = tf.contrib.layers.batch_norm(g2, epsilon=1e-5, scope='bn2')
g2 = tf.nn.relu(g2)
g2 = tf.image.resize_images(g2, [56, 56])
# Gerando 25 features
g_w3 = tf.get_variable('g_w3', [3, 3, z_dim/2, z_dim/4], dtype=tf.float32, initializer=tf.truncated_normal_initializer(stddev=0.02))
g_b3 = tf.get_variable('g_b3', [z_dim/4], initializer=tf.truncated_normal_initializer(stddev=0.02))
g3 = tf.nn.conv2d(g2, g_w3, strides=[1, 2, 2, 1], padding='SAME')
g3 = g3 + g_b3
g3 = tf.contrib.layers.batch_norm(g3, epsilon=1e-5, scope='bn3')
g3 = tf.nn.relu(g3)
g3 = tf.image.resize_images(g3, [56, 56])
# Convolução final com um canal de saída
g_w4 = tf.get_variable('g_w4', [1, 1, z_dim/4, 1], dtype=tf.float32, initializer=tf.truncated_normal_initializer(stddev=0.02))
g_b4 = tf.get_variable('g_b4', [1], initializer=tf.truncated_normal_initializer(stddev=0.02))
g4 = tf.nn.conv2d(g3, g_w4, strides=[1, 2, 2, 1], padding='SAME')
g4 = g4 + g_b4
g4 = tf.sigmoid(g4)
# Dimensões de g4: batch_size x 28 x 28 x 1
return g4
Agora que já definimos as funções dos modelos discriminativo e generativo, vamos ver o que uma saída de amostra de um gerador não treinado parece.
Precisamos abrir uma sessão TensorFlow e criar um espaço reservado (placeholder) para a entrada do nosso gerador. O shape do espaço reservado será "None, z_dimensões". A palavra-chave None
significa que o valor pode ser determinado no tempo de execução da sessão. Normalmente, nós temos "None" como nossa primeira dimensão para que possamos ter tamanhos de lote variáveis. (Com um tamanho de lote de 50, a entrada para o gerador seria de 50 x 100). Com a opção "None", não precisamos especificar batch_size
até mais tarde.
In [6]:
z_dimensions = 100
z_placeholder = tf.placeholder(tf.float32, [None, z_dimensions])
Agora, criamos uma variável (generate_image_output
) que contém a saída do gerador e também inicializaremos o vetor de ruído aleatório que vamos usar como entrada. A função [np.random.normal ()
] (https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.normal.html) tem três argumentos. O primeiro e o segundo definem o desvio padrão e médio para a distribuição normal (0 e 1 no nosso caso) e o terceiro define a forma do vetor (1 x 100
).
In [7]:
generated_image_output = generator(z_placeholder, 1, z_dimensions)
z_batch = np.random.normal(0, 1, [1, z_dimensions])
Em seguida, inicializamos todas as variáveis, alimentamos o z_batch
no espaço reservado e executamos a sessão.
A função [sess.run ()
] (https://www.tensorflow.org/api_docs/python/tf/Session#run) possui dois argumentos. O primeiro é chamado de argumento "fetches"; define o valor que lhe interessa na computação. No nosso caso, queremos ver qual é a saída do gerador. Se você olhar para o último trecho de código, você verá que a saída da função do gerador é armazenada em generated_image_output
, então usaremos generated_image_output
para o nosso primeiro argumento.
O segundo argumento recebe um dicionário de entradas que são substituídas no grafo quando ele é executado. É aí que nós alimentamos nossos espaços reservados. No nosso exemplo, precisamos alimentar nossa variável z_batch
no z_placeholder
que definimos anteriormente. Como antes, veremos a imagem remodelando-a para os pixels '28 x 28' e mostramos com o PyPlot.
In [8]:
# Visualizando a saída do Generator antes do treinamento
with tf.Session() as sess:
sess.run(tf.global_variables_initializer())
generated_image = sess.run(generated_image_output, feed_dict={z_placeholder: z_batch})
generated_image = generated_image.reshape([28, 28])
plt.imshow(generated_image, cmap='Greys')
Nada foi aprendido, pois não houve treinamento. Agora precisamos treinar os pesos e os bias na rede geradora para converter números aleatórios em dígitos reconhecíveis. Olhemos para as funções de perda e otimização!
Uma das partes mais complicadas sobre a criação e ajuste de GANs é que eles têm duas funções de perda: uma que incentiva o gerador a criar imagens melhores e a outra que encoraja o discriminador a distinguir imagens geradas de imagens reais.
Treinamos tanto o gerador quanto o discriminador simultaneamente. À medida que o discriminador melhora em distinguir imagens reais de imagens geradas, o gerador é capaz de melhor ajustar seus pesos e distorções para gerar imagens convincentes.
Aqui estão as entradas e saídas para nossas redes.
In [9]:
tf.reset_default_graph()
batch_size = 50
# Input noise no modelo generativo
z_placeholder = tf.placeholder(tf.float32, [None, z_dimensions], name='z_placeholder')
# Input image no modelo discriminativo
x_placeholder = tf.placeholder(tf.float32, shape = [None,28,28,1], name='x_placeholder')
# Imagens geradas
Gz = generator(z_placeholder, batch_size, z_dimensions)
# Previsões de probabilidade do modelo discriminativo
# Para imagens reais do MNIST
Dx = discriminator(x_placeholder)
# Previsões de probabilidade para as imagens geradas
Dg = discriminator(Gz, reuse_variables=True)
Então, primeiro pensemos sobre o que queremos de nossas redes. O objetivo do discriminador é classificar corretamente as imagens MNIST reais como reais (retornar uma saída com probabilidade mais alta) e gerar imagens como falsas (retornar uma saída mais baixa). Calculamos duas perdas para o discriminador: uma perda que compara Dx
e 1 para imagens reais do conjunto MNIST, bem como uma perda que compara Dg
e 0 para imagens do gerador. Faremos isso com a função [tf.nn.sigmoid_cross_entropy_with_logits ()
] do TensorFlow (https://www.tensorflow.org/api_docs/python/tf/nn/sigmoid_cross_entropy_with_logits), que calcula as perdas entre entropia entre Dx
e 1 e entre Dg
e 0.
sigmoid_cross_entropy_with_logits
opera em valores não escalados em vez de valores de probabilidade de 0 a 1. Dê uma olhada na última linha do nosso discriminador: não há camada softmax ou sigmoid no final. Os GANs podem falhar se seus discriminadores "saturarem", ou se confiar o suficiente para retornar exatamente 0 quando receberem uma imagem gerada; que deixa o discriminador sem um gradiente útil para descer.
A função [tf.reduce_mean ()
] (https://www.tensorflow.org/api_docs/python/tf/reduce_mean) assume o valor médio de todos os componentes na matriz retornada pela função de entropia cruzada. Esta é uma maneira de reduzir a perda para um único valor escalar, em vez de um vetor ou matriz.
In [10]:
d_loss_real = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits = Dx, labels = tf.ones_like(Dx)))
d_loss_fake = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits = Dg, labels = tf.zeros_like(Dg)))
Agora vamos configurar a função de perda do gerador. Queremos que a rede geradora crie imagens que enganarão o discriminador: o gerador quer que o discriminador publique um valor próximo de 1 quando é dada uma imagem do gerador. Portanto, queremos calcular a perda entre Dg
e 1.
In [11]:
g_loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(logits = Dg, labels = tf.ones_like(Dg)))
Agora que temos nossas funções de perda, precisamos definir nossos otimizadores. O otimizador para a rede geradora precisa atualizar apenas os pesos do gerador, e não os do discriminador. Da mesma forma, quando treinamos o discriminador, queremos manter os pesos do gerador resolvidos.
Para fazer essa distinção, precisamos criar duas listas de variáveis, uma com os pesos e os preconceitos do discriminador e outra com os pesos e preconceitos do gerador. É aqui que nomear todas as suas variáveis TensorFlow com um esquema pensativo pode ser útil.
In [12]:
tvars = tf.trainable_variables()
d_vars = [var for var in tvars if 'd_' in var.name]
g_vars = [var for var in tvars if 'g_' in var.name]
print([v.name for v in d_vars])
print([v.name for v in g_vars])
Em seguida, especificamos nossos dois otimizadores. [Adam] (https://www.tensorflow.org/api_docs/python/tf/train/AdamOptimizer) geralmente é o algoritmo de otimização de escolha para GANs; ele utiliza taxas de aprendizagem adaptativa e impulso. Chamamos a função de minimizar de Adão e também especificamos as variáveis que queremos que atualizem - os pesos e os distúrbios do gerador quando treinamos o gerador e os pesos e vies do discriminador quando treinamos o discriminador.
Estamos configurando duas operações de treinamento diferentes para o discriminador aqui: uma que treina o discriminador em imagens reais e uma que treina o discrmnator em imagens falsas. Às vezes, é útil usar diferentes taxas de aprendizagem para essas duas operações de treinamento, ou usá-las separadamente para [regular a aprendizagem de outras maneiras] (https://github.com/jonbruner/ezgan).
In [13]:
# Treinando o modelo discriminativo
d_trainer_fake = tf.train.AdamOptimizer(0.0003).minimize(d_loss_fake, var_list=d_vars)
d_trainer_real = tf.train.AdamOptimizer(0.0003).minimize(d_loss_real, var_list=d_vars)
# Treinando o modelo generativo
g_trainer = tf.train.AdamOptimizer(0.0001).minimize(g_loss, var_list=g_vars)
Pode ser complicado fazer com que os GANs convergem e, além disso, muitas vezes eles precisam treinar por muito tempo. [TensorBoard] (https://www.tensorflow.org/how_tos/summaries_and_tensorboard/) é útil para rastrear o processo de treinamento; Ele pode representar propriedades escalares como perdas, exibir imagens de amostra durante o treino e ilustrar a topologia das redes neurais.
Se você executar este script em sua própria máquina, inclua a célula abaixo. Então, em uma janela do terminal do diretório em que este caderno reside, execute
tensorboard --logdir=tensorboard/
e abra o TensorBoard visitando [http: // localhost: 6006
] (http: // localhost: 6006) em seu navegador da Web.
In [14]:
# Variáveis
tf.get_variable_scope().reuse_variables()
tf.summary.scalar('Generator_loss', g_loss)
tf.summary.scalar('Discriminator_loss_real', d_loss_real)
tf.summary.scalar('Discriminator_loss_fake', d_loss_fake)
images_for_tensorboard = generator(z_placeholder, batch_size, z_dimensions)
tf.summary.image('Generated_images', images_for_tensorboard, 5)
merged = tf.summary.merge_all()
logdir = "tensorboard/" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + "/"
writer = tf.summary.FileWriter(logdir, sess.graph)
E agora nós iteramos. Começamos dando brevemente ao discriminador algum treinamento inicial; Isso ajuda a desenvolver um gradiente útil para o gerador.
Em seguida, avançamos para o ciclo principal de treinamento. Quando formamos o gerador, alimentaremos um vetor 'z' aleatório no gerador e passamos a saída para o discriminador (esta é a variável Dg
que especificamos anteriormente). Os pesos e bias do gerador serão atualizados para produzir imagens que o discriminador é mais provável que classifique como real.
Para treinar o discriminador, alimentaremos um lote de imagens do conjunto MNIST para servir como exemplos positivos e, em seguida, treinamos o discriminador novamente em imagens geradas, usando-os como exemplos negativos. Lembre-se de que, à medida que o gerador melhora a sua saída, o discriminador continua a aprender a classificar as imagens do gerador melhoradas como falsas.
Se você quiser executar este código, prepare-se para aguardar: demora cerca de 40 minutos em uma GPU, mas pode demorar dez vezes mais em uma CPU desktop.
In [15]:
sess = tf.Session()
sess.run(tf.global_variables_initializer())
# Pré-treino do modelo discriminativo
for i in range(300):
z_batch = np.random.normal(0, 1, size=[batch_size, z_dimensions])
real_image_batch = mnist.train.next_batch(batch_size)[0].reshape([batch_size, 28, 28, 1])
_, __, dLossReal, dLossFake = sess.run([d_trainer_real, d_trainer_fake, d_loss_real, d_loss_fake],
{x_placeholder: real_image_batch, z_placeholder: z_batch})
if(i % 100 == 0):
print("dLossReal:", dLossReal, "dLossFake:", dLossFake)
# Treinar os modelos discriminativo e generativo juntos
for i in range(100000):
real_image_batch = mnist.train.next_batch(batch_size)[0].reshape([batch_size, 28, 28, 1])
z_batch = np.random.normal(0, 1, size=[batch_size, z_dimensions])
# Treinando o modelo discriminativo nos dados reais e fake
_, __, dLossReal, dLossFake = sess.run([d_trainer_real, d_trainer_fake, d_loss_real, d_loss_fake],
{x_placeholder: real_image_batch, z_placeholder: z_batch})
# Treinando o modelo generativo
z_batch = np.random.normal(0, 1, size=[batch_size, z_dimensions])
_ = sess.run(g_trainer, feed_dict={z_placeholder: z_batch})
if i % 10 == 0:
# Atualiza o TensorBoard com Estatísticas
z_batch = np.random.normal(0, 1, size=[batch_size, z_dimensions])
summary = sess.run(merged, {z_placeholder: z_batch, x_placeholder: real_image_batch})
writer.add_summary(summary, i)
if i % 100 == 0:
# A cada 100 iterações mostra uma imagem gerada
print("Iteração:", i, "at", datetime.datetime.now())
z_batch = np.random.normal(0, 1, size=[1, z_dimensions])
generated_images = generator(z_placeholder, 1, z_dimensions)
images = sess.run(generated_images, {z_placeholder: z_batch})
plt.imshow(images[0].reshape([28, 28]), cmap='Greys')
plt.show()
# Show discriminator's estimate
im = images[0].reshape([1, 28, 28, 1])
result = discriminator(x_placeholder)
estimate = sess.run(result, {x_placeholder: im})
print("Estimativa:", estimate)
Os GANs são notoriamente difíceis de treinar. Sem hiperparâmetros certos, arquitetura de rede e procedimento de treinamento, o discriminador pode dominar o gerador ou vice-versa.
Em um modo de falha comum, o discriminador supera o gerador, classificando as imagens geradas como falsas com certeza absoluta. Quando o discriminador responde com absoluta certeza, não deixa nenhum gradiente para o gerador descer. Isto ocorre, em parte, porque construímos o nosso discriminador para produzir um output não padronizado, em vez de passar a sua saída através de uma função sigmoide que empurraria a avaliação para 0 ou 1.
Em outro modo de falha comum conhecido como colapso do modo , o gerador descobre e explora alguma fraqueza no discriminador. Você pode reconhecer o colapso do modo em seu GAN se ele gera muitas imagens muito semelhantes, independentemente da variação na entrada do gerador z. O colapso do modo às vezes pode ser corrigido por "fortalecer" o discriminador de alguma maneira - por exemplo, ajustando sua taxa de treinamento ou reconfigurando suas camadas.